/**
* @file StreamingSound.cpp
* @author Max Ocklind
* @version 2010-01-10
*
* An easy-to-use wrapper for an OpenAL streaming Ogg Vorbis sound source with some extra sound
* properties:</br>
* * sound priprity</br>
* * distance culling type</br>
* * sound type</br>
* * sound effect filter with an associated timer
*
* <p><b>Note:</b> if the default constructor is used the <code>StreamingSound</code> object must be
* reseted (provided with a sound source) before it can be used!</p>
*
* <p>Based on the source and documentation by Jesse Maurais found at:
* <a href="http://www.devmaster.net/articles/openal-tutorials/lesson8.php">
* http://www.devmaster.net/articles/openal-tutorials/lesson8.php</a>
* </p>
**/


/* includes */

#include "StreamingSound.h"


/* functions */

/* public */

/**
* Starts streaming and plays the specified Ogg Vorbis file with default values and without any sound
* effect filter enabled.
*
* @param path the path to the Ogg Vorbis file to be streamed and played
* @param priority the priority of the streaming sound to be played
* @param cullType the culling type of the streaming sound to be played
* @param position pointer to floating point-vector
* @param velocity pointer to floating point-vector
* @throws string if the specified Ogg Vorbis file cannot be opened or streamed
*/
void StreamingSound::play(std::string filePath, int priority, const char* cullType, ALfloat* position, ALfloat* velocity)
{
	play(filePath, priority, cullType, position, velocity, DEFAULT_AL_LOOP, DEFAULT_AL_PITCH, DEFAULT_AL_GAIN, DEFAULT_AL_ROLLOFF_FACTOR, NULL, 0);
}

/**
* Starts streaming and plays the specified Ogg Vorbis file with default values and with the specified
* sound effect filter enabled for the specified number of seconds.
*
* @param path the path to the Ogg Vorbis file to be streamed and played
* @param priority the priority of the streaming sound to be played
* @param cullType the culling type of the streaming sound to be played
* @param position pointer to floating point-vector
* @param velocity pointer to floating point-vector
* @param filter a pointer to the sound effect filter that should be enabled on the source, 
* <code>NULL</code> is used to play source without any sound effect filter enabled
* @param timeLimit the time in seconds that the sound effect filter should be enabled
* @throws string if the specified Ogg Vorbis file cannot be opened or streamed
*/
void StreamingSound::play(std::string filePath, int priority, const char* cullType, ALfloat* position, ALfloat* velocity, SFXFilter* filter, unsigned long timeLimit)
{
	play(filePath, priority, cullType, position, velocity, DEFAULT_AL_LOOP, DEFAULT_AL_PITCH, DEFAULT_AL_GAIN, DEFAULT_AL_ROLLOFF_FACTOR, filter, timeLimit);
}

/**
* Starts streaming and plays the specified Ogg Vorbis file with specified values and with the
* specified sound effect filter enabled for the specified number of seconds.
*
* @param path the path to the Ogg Vorbis file to be streamed and played
* @param priority the priority of the streaming sound to be played
* @param cullType the culling type of the streaming sound to be played
* @param position pointer to floating point-vector
* @param velocity pointer to floating point-vector
* @param loop indicates if the streaming sound should loop or not
* @param pitch the pitch of the streaming sound (0.0 - ...)
* @param gain the gain (contribution; 0.0 - 1.0) to the over all sound output
* @param rolloffFactor specifies how fast the streaming sound should fade with distance (0.0 - ...)
* @param filter a pointer to the sound effect filter that should be enabled on the source, 
* <code>NULL</code> is used to play source without any sound effect filter enabled
* @param timeLimit the time in seconds that the sound effect filter should be enabled
* @throws string if the specified Ogg Vorbis file cannot be opened or streamed
*/
void StreamingSound::play(std::string filePath, int priority, const char* cullType, ALfloat* position, ALfloat* velocity, ALboolean loop, ALfloat pitch, ALfloat gain, ALfloat rolloffFactor, SFXFilter* filter, unsigned long timeLimit)
{
	// make sure sound is stopped
	stop();

	// open file and stream
	mOpen(filePath);
	
	// update priority and distance culling type
	mPriority = priority;
	mCullType = cullType;

	// disable any active sound effect filter to make sure that the correct sound properties are
	// stored if enableing a sound effect filter
	disableSFXFilter();

	// set basic sound properties
	setProperties(position, velocity, loop, pitch, gain, rolloffFactor);

	// enable sound effect filter if available
	if (filter)
		enableSFXFilter(filter, timeLimit);

	// play sound
	mPlayback();
}

/**
* This function is not supported by StreamingSound, use function <code>play(std::string path, ...</code>
* instead.
*
* @throws string unsupported function
*/
void StreamingSound::play(SoundBuffer buffer, int priority, const char* cullType, ALfloat* position, ALfloat* velocity)
{
	throw std::string("Unsupported function, use function play(std::string path, ... instead.");
}

/**
* This function is not supported by StreamingSound, use function <code>play(std::string path, ...</code>
* instead.
*
* @throws string unsupported function
*/
void StreamingSound::play(SoundBuffer buffer, int priority, const char* cullType, ALfloat* position, ALfloat* velocity, SFXFilter* filter, unsigned long timeLimit)
{
	throw std::string("Unsupported function, use function play(std::string path, ... instead.");
}

/**
* This function is not supported by StreamingSound, use function <code>play(std::string path, ...</code>
* instead.
*
* @throws string unsupported function
*/
void StreamingSound::play(SoundBuffer buffer, int priority, const char* cullType, ALfloat* position, ALfloat* velocity, ALboolean loop, ALfloat pitch, ALfloat gain, ALfloat rolloffFactor, SFXFilter* filter, unsigned long timeLimit)
{
	throw std::string("Unsupported function, use function play(std::string path, ... instead.");
}

/**
* Sets the basic sound properties of the streaming sound source.
*
* @param position pointer to floating point-vector
* @param velocity pointer to floating point-vector
* @param loop indicates if the streaming sound should loop or not
* @param pitch the pitch of the sound (0.0 - ...)
* @param gain the gain (volume contribution, 0.0 - 1.0) to the over all sound output
* @param rolloffFactor specifies how fast the streaming sound should fade with distance (0.0 - ...)
*/
void StreamingSound::setProperties(ALfloat *position, ALfloat *velocity, ALboolean loop, ALfloat pitch, ALfloat gain, ALfloat rolloffFactor)
{
	// looping handled by StreamingSound rather than by OpenAL
	Sound::setProperties(position, velocity, AL_FALSE, pitch, gain, rolloffFactor);
	mLooping = loop;
}

/**
* Updates the buffers used for streaming by loading new stream data into the buffers that have
* already been processed and reenqueuing them to the end of the streaming buffer queue.
*/
void StreamingSound::update()
{
	// check and update sound effect filter
	Sound::update();

	int			processed;
	SoundBuffer buffer;
    bool		streamDataLoaded = true;

	// get number of processed buffers in the buffer queue
    alGetSourcei(mSource, AL_BUFFERS_PROCESSED, &processed);
	errCheck->checkALError("StreamingSound", "update (get processed buffers)");

    while (processed--)
    {        
        alSourceUnqueueBuffers(mSource, 1, &buffer);
        errCheck->checkALError("StreamingSound", "update (unqueue processed buffer)");

		// load new stream data into processed buffer if possible
        streamDataLoaded = mStream(buffer);

		// re-enqueue processed buffer if stream data could be loaded
		if (streamDataLoaded)
		{
			alSourceQueueBuffers(mSource, 1, &buffer);
			errCheck->checkALError("StreamingSound", "update (queue processed buffer)");
		}
    }

	// handle looping when the last of the stream data has been loaded
	if (mLooping && !streamDataLoaded)
	{
		// wait for last buffer to be processed
		while (processed != 1)
		{
			alGetSourcei(mSource, AL_BUFFERS_PROCESSED, &processed);
			errCheck->checkALError("StreamingSound", "update (get processed buffers looping)");
		}

		// reopen file, reload stream and play
		mRelease();
		mOpen(mFilePath);
		mPlayback();
	}
}

/**
* Resets the properties of the sound source and replaces the current sound source with the
* specified sound source to allow reusage of this <code>StaticSound</code>-object.
*
* @param source the new sound source to replace the current one
*/
void StreamingSound::reset(SoundSource source)
{
	Sound::reset(source);
	mStreamOpened	   = false;
	mHasBegunBuffering = false;
	mSoundType		   = STREAMING_SOUND;
	mRelease();
}


/* protected */

/**
* Reads from the Ogg Voribis file located at the specified file path and opens the Ogg Vorbis stream.
*
* @param filePath the path to the Ogg Vorbis file to be read from
* @throws string if the specified file or stream could not be read or opened
*/
void StreamingSound::mOpen(std::string filePath)
{
	// stop and release any previously loaded Ogg Vorbis stream
	if (mStreamOpened)
		mRelease();
    
	// try to open Ogg file
    if (!(mOggFile = fopen(filePath.c_str(), "rb")))
	{
		std::stringstream ss;
		ss << "failed to open Ogg Vorbis file \"" << filePath << "\"";
		errCheck->throwError("Ogg Stream", "StreamingSound", "open", ss.str().c_str());
	}

	// try to open Ogg stream
    if (ov_open(mOggFile, &mOggStream, NULL, 0) < 0)
    {
		// close file and throw error
        fclose(mOggFile);
		std::stringstream ss;
		ss << "failed to open Ogg Vorbis stream from file \"" << filePath << "\"";
		errCheck->throwError("Ogg Stream", "StreamingSound", "open", ss.str().c_str());
    }

	// get Ogg Vorbis info
    mVorbisInfo = ov_info(&mOggStream, -1);

	// set correct channel mode
    if (mVorbisInfo->channels == 1)
        mFormat = AL_FORMAT_MONO16;
    else
        mFormat = AL_FORMAT_STEREO16;
    
	mFilePath		   = filePath;
	mStreamOpened	   = true;
	mHasBegunBuffering = false;
}

/**
* Releases the open Ogg Vorbis stream and empties the buffers used for streaming.
*/
void StreamingSound::mRelease()
{
	// do nothing if the Ogg Vorbis stream has not been loaded
	if (!mStreamOpened)
		return;

	// stop sound and empty buffer queue
    stop();
    mEmpty();

	// clear Ogg stream
    ov_clear(&mOggStream);

	mStreamOpened = false;
}

/**
* Starts to buffer and play the streaming sound.
*
* @return <code>true</code> if the sound successfully starts to play or is already playing, otherwise <code>false</code>
*/
bool StreamingSound::mPlayback()
{
	// do nothing if it is already playing
	if (isPlaying())
		return true;
	
	if (!mHasBegunBuffering)
	{
		// load stream data into buffers
		for (int i = 0; i < NUM_STREAM_BUFFERS; i++)
			if (!mStream(mBuffers[i]))
			   return false;

		// enqueue buffers
		alSourceQueueBuffers(mSource, NUM_STREAM_BUFFERS, mBuffers);
		errCheck->checkALError("StreamingSound", "playback (queue buffers)");
	}

	mHasBegunBuffering = true;

	// play sound
    alSourcePlay(mSource);
	errCheck->checkALError("StreamingSound", "playback (play)");
    
    return true;
}


/**
* Loads stream data into the specified buffer.
*
* @param buffer the buffer to load streami data into
*/
bool StreamingSound::mStream(SoundBuffer buffer)
{
    char pcm[STREAM_BUFFER_SIZE];
    int  size = 0;
    int  section;
    int  result;

	// load stream data
    while (size < STREAM_BUFFER_SIZE)
    {
		// read stream data
        result = ov_read(&mOggStream, pcm + size, STREAM_BUFFER_SIZE - size, 0, 2, 1, &section);
    
        if (result > 0)
            size += result;
        else if (result < 0)
			errCheck->throwError("Ogg Stream", "StreamingSound", "stream", mErrorString(result).c_str());
		else
			break;
    }
    
	// end of stream reached
    if (size == 0)
        return false;
        
	// buffer the loaded stream data
    alBufferData(buffer, mFormat, pcm, size, mVorbisInfo->rate);
    errCheck->checkALError("StreamingSound", "stream (buffer data)");
    
    return true;
}

/**
* Empties the buffer queue of stream data.
*/
void StreamingSound::mEmpty()
{
    int queued;
	SoundBuffer buffer;
    
	// get number of queued buffers
    alGetSourcei(mSource, AL_BUFFERS_QUEUED, &queued);
	errCheck->checkALError("StreamingSound", "empty (buffers queued)");
    
	// unqueue queued buffers
    while (queued--)
    {
        alSourceUnqueueBuffers(mSource, 1, &buffer);
        errCheck->checkALError("StreamingSound", "empty (unqueue buffer)");
    }
}

/**
* Fetches a string description of the specified error code.
*
* @param errorCode the error code to fetch the description for
* @return a string description of the specified error code
*/
std::string StreamingSound::mErrorString(int errorCode)
{
    switch (errorCode)
    {
	case OV_EREAD:
		return std::string("Read from media.");
    case OV_ENOTVORBIS:
        return std::string("Not Vorbis data.");
    case OV_EVERSION:
        return std::string("Vorbis version mismatch.");
    case OV_EBADHEADER:
        return std::string("Invalid Vorbis header.");
    case OV_EFAULT:
        return std::string("Internal logic fault (bug or heap/stack corruption.");
    default:
        return std::string("Unknown Ogg error.");
    }
}